/* DefaultDowryService.java
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

package dowry.impl;

import java.beans.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import dowry.*;
import dowry.datatype.*;

/**
 * DefaultDowryService provides a basic implementation of
 * DowryService.
 *
 */
public class DefaultDowryService implements DowryService
{
    public static final String JS_EXTENSION = ".js";
    public static final String IL8N_JS_EXTENSION = ".il8n.js";

    private DatatypeManager datatypeManager  = null;
    private EntityMapper    entityMapper     = null;

    /**
     * Creates an instance of the service.
     *
     */
    public DefaultDowryService()
    {
    }

    public DatatypeManager getDatatypeManager()
    {
        DatatypeManager datatypeManager = null;

        if (this.datatypeManager == null)
        {
            datatypeManager = DowryContext.getDatatypeManager();
        }
        else
        {
            datatypeManager = this.datatypeManager;
        }

        return datatypeManager;
    }

    public EntityMapper getEntityMapper()
    {
        EntityMapper entityMapper = null;

        if (this.entityMapper == null)
        {
            entityMapper = DowryContext.getEntityMapper();
        }
        else
        {
            entityMapper = this.entityMapper;
        }

        return entityMapper;
    }

    /**
     * Returns Javascript resources using the classloader.
     *
     * @param  className the javascript classname
     *
     * @return           the javascript as a string
     *
     */
    public String loadJs(String className)
    {
        String js = null;

        if (className != null)
        {
            className = "/" + className.replace('.', '/');

            InputStream raw =
                Thread.currentThread()
                      .getContextClassLoader()
                      .getResourceAsStream(className+JS_EXTENSION);

            if (raw != null)
            {
                BufferedReader in = null;

                try
                {
                    in = new BufferedReader(new InputStreamReader(raw));

                    StringBuffer sb = new StringBuffer();

                    String line;
                    while ((line = in.readLine()) != null)
                    {
                        sb.append(line + "\n");
                    }

                    js = sb.toString();
                }
                catch(IOException ioex)
                {
                    // suppress exception and return null
                }
                catch(NullPointerException npex)
                {
                    // suppress exception and return null
                }
                finally
                {
                    if (in != null)
                    {
                        try
                        {
                            in.close();
                        }
                        catch(Throwable t)
                        {
                        }
                    }
                }

                if (js != null)
                {
                    String il8n = loadInternationalization(
                        className, Locale.getDefault());

                    if (il8n != null)
                    {
                        js = js + "\n" + il8n;
                    }
                }
            }

        }

        return js;
    }

    public void setDatatypeManager(DatatypeManager datatypeManager)
    {
        this.datatypeManager = datatypeManager;
    }

    public void setEntityMapper(EntityMapper entityMapper)
    {
        this.entityMapper = entityMapper;
    }

    /**
     * Validates the provided configuration using the
     * appropriate Dowry datatype.
     *
     * @param  cfg  a map containing all the configuration
     *              information relevant to the local
     *              representation of this datatype
     *
     * @return      the string validation error message,
     *              if the validation fails; null otherwise
     *
     */
    public String validate(HashMap cfg)
    {
        String msg = null;

        DatatypeManager datatypeMgr = getDatatypeManager();
        if (datatypeMgr != null && cfg != null)
        {
            // first try by type...
            String type = (String) cfg.get(AbstractType.TYPE);
            Datatype datatype = datatypeMgr.getDatatypeByType(type);

            // didn't find it by type, see if we
            // can find it by java class...
            if (datatype == null)
            {
                Class javaClass = getJavaClass(cfg);
                datatype = datatypeMgr.getDatatype(javaClass);
            }

            if (datatype != null)
            {
                msg = datatype.validate(cfg);
            }
        }

        return msg;
    }

    /**
     * Validates a JavaBean object using the configuration
     * information provided by the entity mapper for the
     * specified entity name.  It wll return a map
     * containing the results in the format: bean property
     * name => message.
     *
     * @param bean        the JavaBean object to be
     *                    validated
     *
     * @param entityName  the entity to validate this object
     *                    as
     *
     * @return            a map containing any validation
     *                    errors
     *
     */
    public Map validateBean(Object bean, String entityName)
    {
        Map replyMap = Collections.EMPTY_MAP;

        EntityMapper entityMapper = getEntityMapper();
        if (entityMapper != null)
        {
            Map mapping = entityMapper.getEntityMapping();
            if (mapping != null)
            {
                Map properties = (Map) mapping.get(entityName);
                if (properties != null)
                {
                    replyMap = validateBean(bean, properties);
                }
            }
        }

        return replyMap;
    }

    /**
     * Validates a JavaBean object using the raw
     * configuration information provided.  This
     * configuration information should be in the entity
     * mapper format of:
     *
     * <pre>
     * Map&lt;String property,
     *     Map&lt;String attribute, Object value&gt;&gt;
     * </pre>
     *
     * <p>
     * It wll return a map containing the results in the
     * format: bean property name => message.
     * </p>
     *
     * @param bean        the JavaBean object to be
     *                    validated
     *
     * @param properties  a map with the entity's properties
     *
     * @return            a map containing any validation
     *                    errors
     *
     */
    public Map validateBean(Object bean, Map properties)
    {
        Map replyMap = new HashMap();

        if (bean != null && properties != null)
        {
            Iterator entryIter = properties.entrySet().iterator();
            while (entryIter.hasNext())
            {
                Map.Entry entry = (Map.Entry) entryIter.next();
                String id = (String) entry.getKey();
                Map cfg = (Map) entry.getValue();

                Object val = getPropertyValue(bean, id);

                HashMap cfgWithValue = new HashMap(cfg);
                cfgWithValue.put(AbstractType.VALUE, val);

                String msg = validate(cfgWithValue);
                replyMap.put(id, msg);
            }
        }

        return replyMap;
    }

    /**
     * Performs multiple validations as a single operation.
     * It wll return a map containing the results in the
     * format: id => message.
     *
     * @param  cfg  an array of maps, each containing all
     *              the configuration information relevant
     *              to the local representation of this
     *              datatype
     *
     * @return      a map containing keys representing the
     *              string id of this configuration, mapped
     *              to the string validation error message
     *
     */
    public Map validateMany(HashMap[] cfg)
    {
        Map replyMap = new HashMap();

        if (cfg != null)
        {
            for (int i=0; i < cfg.length; i++)
            {
                HashMap currentCfg = cfg[i];
                if (currentCfg != null)
                {
                    String id = (String) currentCfg.get(AbstractType.ID);
                    if (id != null)
                    {
                        String msg = validate(currentCfg);
                        replyMap.put(id, msg);
                    }
                }
            }
        }

        return replyMap;
    }

    /**
     * Gets the Java class from the Map as a Class object.
     *
     * @param  cfg  a map containing all the configuration
     *              information relevant to the local
     *              representation of this datatype
     *
     * @return        the class object from the
     *                configuration
     */
    protected Class getJavaClass(Map cfg)
    {
        Class c = null;

        try
        {
            Object o = cfg.get(AbstractType.JAVA_CLASS);
            if (o instanceof Class)
            {
                c = (Class) o;
            }
            else if (o instanceof String)
            {
                String s = (String) o;
                c = Class.forName(s);
            }
        }
        catch(ClassNotFoundException ex)
        {
        }

        return c;
    }

    /**
     * Gets the value of the specified property off of the
     * specified JavaBean object.
     *
     * @param  bean       the JavaBean object containing the
     *                    specified property
     *
     * @param  property   the string name of the property
     *
     * @return            the property value
     *
     */
    protected Object getPropertyValue(Object bean, String property)
    {
        Object val = null;

        try
        {
            if (bean != null && property != null)
            {
                PropertyDescriptor pd =
                    new PropertyDescriptor(property, bean.getClass());

                Method readMethod = pd.getReadMethod();
                val = readMethod.invoke(bean, new Object[0]);
            }
        }
        catch(IllegalAccessException iaex)
        {
        }
        catch(IntrospectionException iex)
        {
        }
        catch(InvocationTargetException itex)
        {
        }

        return val;
    }

    /**
     * Loads the most internationalized "patch" provided for
     * the specified resource.  The internationalization
     * will occur by searching for a specifed script using
     * the pattern:
     *
     * <pre>
     * name + "_" + language + "_" + country + "_" + variant + ".il8n.js"
     * name + "_" + language + "_" + country + ".il8n.js"
     * name + "_" + language + ".il8n.js"
     * name + ".il8n.js"
     * </pre>
     *
     * So, if you were trying to load the Javascript class
     * <code>org.example.Foo</code>, then the
     * internationalization script might be searched for
     * like:
     *
     * <pre>
     * /org/example/Foo_de_DE_EURO.il8n.js
     * /org/example/Foo_de_DE.il8n.js
     * /org/example/Foo_de.il8n.js
     * /org/example/Foo.il8n.js
     * </pre>
     *
     * @param  resource  the String name of the resource,
     *                   as a resource path (the
     *                   fully-qualified classname converted
     *                   to using forward slashes, etc)
     *
     * @param  locale    the locale to internationalize to
     *
     * @return           the script containing the
     *                   internationalization "patch", if
     *                   found; otherwise, null
     */
    protected String loadInternationalization(String resource, Locale locale)
    {
        String js = null;

        if (resource != null && locale != null)
        {
            String language = locale.getLanguage();
            String country = locale.getCountry();
            String variant = locale.getVariant();

            String searchOrder[] = new String[4];
            searchOrder[3] = IL8N_JS_EXTENSION;
            searchOrder[2] = "_" + language + IL8N_JS_EXTENSION;
            if (country != null && !country.trim().equals(""))
            {
                searchOrder[1] = "_" + language +
                                 "_" + country +
                                 IL8N_JS_EXTENSION;

                if (variant != null && !variant.trim().equals(""))
                {
                    searchOrder[0] = "_" + language +
                                     "_" + country +
                                     "_" + variant +
                                     IL8N_JS_EXTENSION;
                }
            }

            for (int i=0; i < searchOrder.length; i++)
            {
                if (searchOrder[i] != null)
                {

                    InputStream raw =
                        Thread.currentThread()
                              .getContextClassLoader()
                              .getResourceAsStream(resource+searchOrder[i]);

                    BufferedReader in = null;

                    try
                    {
                        in = new BufferedReader(new InputStreamReader(raw));

                        StringBuffer sb = new StringBuffer();

                        String line;
                        while ((line = in.readLine()) != null)
                        {
                            sb.append(line + "\n");
                        }

                        js = sb.toString();
                        break;
                    }
                    catch(IOException ioex)
                    {
                        // suppress exception... try again on
                        // the next match?
                    }
                    catch(NullPointerException npex)
                    {
                        // suppress exception... try again on
                        // the next match?
                    }
                    finally
                    {
                        if (in != null)
                        {
                            try
                            {
                                in.close();
                            }
                            catch(Throwable t)
                            {
                            }
                        }
                    }
                }
            }
        }

        return js;
    }
}